Skip to content

Recipe Read Guard Metadata Command Remediation#4213

Merged
Trecek merged 8 commits into
developfrom
recipe-read-guard-safe-metadata-4207
Jul 8, 2026
Merged

Recipe Read Guard Metadata Command Remediation#4213
Trecek merged 8 commits into
developfrom
recipe-read-guard-safe-metadata-4207

Conversation

@Trecek

@Trecek Trecek commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix recipe read guard false positives by centralizing protected-path command classification for hook and server-side command execution. The change permits narrow non-content metadata/staging operations against protected recipe, skill, and agent paths while continuing to deny direct content reads and shell bypasses.

The implementation adds shared classification coverage for safe git add, metadata-only git diff, safe git status, and wc -l usage, and blocks content-producing Git modes, heredocs, substitutions, variable reuse in command chains, and unsafe chained reads. Hook and MCP run_cmd behavior now use the same classifier and focused regression tests cover the allow and deny cases.

Closes #4207

Implementation Plan

Plan file: /home/talon/projects/generic_automation_mcp/.autoskillit/temp/codex-loop/4207-manual-remediation-plan.md

🤖 Generated with Claude Code via AutoSkillit

Token Usage Summary

Step Model count uncached output cache_read peak_ctx turns cache_write time
prepare_pr* MiniMax-M3 1 76.1k 1.4k 287.6k 0 13 0 31s
compose_pr* MiniMax-M3 1 35.8k 2.5k 175.4k 0 12 0 43s
review_pr* MiniMax-M3 2 221.2k 87.8k 3.1M 0 123 0 17m 24s
resolve_review* MiniMax-M3 2 221.4k 56.7k 10.3M 107.6k 236 0 26m 50s
Total 554.5k 148.5k 13.9M 107.6k 0 45m 28s

* Step used a non-Anthropic provider; caching behavior may differ.

Token Efficiency

Step LoC Changed cache_read/LoC cache_write/LoC output/LoC
prepare_pr 0
compose_pr 0
review_pr 0
resolve_review 102 101388.8 0.0 556.3
Total 102 136540.1 0.0 1455.6

Model Usage Breakdown

Model steps uncached output cache_read cache_write time
MiniMax-M3 4 554.5k 148.5k 13.9M 0 45m 28s

Trecek added 8 commits July 8, 2026 13:15
Reviewer findings addressed:

* is_allowed_protected_path_metadata_command (status branch): drop
  redundant flag.startswith('-v') check — _GIT_STATUS_CONTENT_FLAGS
  already enumerates {-v, --verbose}, so the prefix check only added
  future-flag fragility (any hypothetical -vX would have been caught
  but should not be).

* is_allowed_protected_path_metadata_command (wc branch): match the
  git branch's verb.endswith('/git') pattern with verb.endswith('/wc')
  so /usr/bin/wc and command wc are recognized symmetrically.

* wc branch flag allowlist: replace the confusing
  'token == --lines or ('l' in token and not set(token.lstrip('-')) -
  {"l"})' expression with a single _is_allowed_wc_flag() helper
  driven by a re.fullmatch pattern (-l+|--lines). Reject
  --lines=10 (value-bearing) and -lL (compound) cleanly while keeping
  -l, -ll, --lines allowlisted.
The deny parametrize case had an unbalanced open-paren without a close. The shlex
tokenizer raises ValueError on unbalanced input and returns an empty list, so the
deny assertion only passed because the raw substitution regex fires on the literal
string before shlex parse. A future refactor that normalizes via shlex first
would silently flip behavior on this input.

Closing the paren and quoting the substitution makes the input reflect a realistic
adversarial payload while still exercising the same substitution branch.
Two server-test improvements from the review:

* test_allows_recipe_path_vcs_and_metadata_commands: in addition to checking
  success=True, assert that subtype is absent and the recipe-read deny trigger
  is not present in result. A guard that always returns allow (i.e. never
  triggers the deny path) would also pass the success-only check, so the
  negative assertions catch a false-negative regression.

* test_denies_protected_path_content_bypasses: annotate the substitution and
  state-var cases with the bypass class they target so a future parser refactor
  knows to preserve these specific deny paths.
The previous commit added 'assert RECIPE_READ_DENY_TRIGGER not in result["result"]'
but successful run_cmd results do not contain a 'result' key — that key is only
present on the deny path. The subtype-absent check is sufficient to distinguish a
true positive (allowed through the metadata-allow path) from a false negative
(allowed because the deny path silently bypassed).
The 'git add' content-flag allowlist omitted -A, --all, --force,
--no-ignore-removal, and --no-all. 'git add -A -- src/.../foo.yaml'
was therefore classified as metadata (allowed) but actually stages
content for indirect read via 'git diff --staged'.
'is_allowed_protected_path_metadata_command' (and the broader
git/wc/verb detection helpers) denied legitimate metadata commands
prefixed with 'command', 'nice', 'time', 'sudo', 'nohup', 'timeout N',
and 'stdbuf -o0' because '_command_start_index' only stripped 'env'
and env-var assignments. The wrapper token became the verb and the
real command (git/wc/...) was never inspected.

'xargs' is intentionally excluded: adding it would let
'xargs cat src/.../foo.yaml' reach the downstream reader check,
weakening the xargs-chain bypass detection (DISCUSS in iter_1).
…ands, and content-staging flags

Add parametrize cases to test_denies_protected_path_content_bypasses:
* cat < path (input redirection)
* git show/log -p/blame/grep with protected path (reader subcommands
  not in the metadata allowlist)
* git add -A / --all with a restricted path (stages content for
  indirect read via 'git diff --staged')

Pairs with the _GIT_ADD_CONTENT_FLAGS expansion; locks the corrected
behavior so a future regression that drops the new flags will fail
the suite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant